﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using LiSA_Service.ResearchProjects.Models;

namespace LiSA_Service.ResearchProjects.Actioners
{
    public class BibTexParser
    {
        /// <summary>
        /// Parses a BibTex string
        /// </summary>
        /// <param name="bibTexString">The BibTex string</param>
        /// <returns>Success indicator</returns>
        public List<Article> ParseBibTex(string bibTexString)
        {
            // We want to split at the '@' symbol, but it used for multiple cases. So we split at each and then sort away the special-cases: 
            // @comment, @string and @preamble
            // This will all fail miserably if there are email addresses in the bibtex file. We assume there aren't.
            // TODO: Fix E-mail issue
            if (bibTexString == null)
            {
                return new List<Article>();
            }
            // Split string into each BibTex entry
            string[] splitArray;
            try
            {
                splitArray = bibTexString.Split('@');
            }
            catch (Exception)
            {
                return new List<Article>();
            }

            // Array for all articles found in the BibTex string
            var allArticles = new List<Article>();


            var replacementStrings = new Dictionary<string, string>();
            // Next, iterate through article, splitting at commas to find the components. Foreach useful component, extract relevant information and insert into an Article object
            // We'll treat this as  finite-state machine as it allows for more robustness against malformed BibTex code and ensures extensibility
            foreach (var article in splitArray)
            {
                // Declaration of variables to keep track of where we are in the FSM
                int atChar, arrayLength;
                char[] charArray;
                var fields = new Dictionary<string, string>();
                // Clean string and turn into CharArray
                charArray = (article.Replace("@", "")).Replace("/r", "").Replace("\r", "").Replace("/n", "").Replace("\n", "").Trim().ToCharArray();
                var newArticle = new Article { ID = Guid.NewGuid(), Fields = new Collection<ArticleField>(), Type = "Journal"};

                // Build the Type
                atChar = 0;
                arrayLength = charArray.Length;
                var workingString = string.Empty;
                while (atChar < arrayLength)
                {
                    if (charArray[atChar].Equals('{'))
                    {
                        atChar++;
                        break;
                    }
                    workingString += charArray[atChar];
                    atChar++;
                }

                if (string.Equals(workingString.Trim().ToLower(), "comment"))
                {
                    // Ignore comments for the time being
                    continue;
                }
                if (string.Equals(workingString.Trim().ToLower(), "preamble"))
                {
                    // Ignore preambles for the time being
                    continue;
                }
                if (string.Equals(workingString.Trim().ToLower(), "string"))
                {
                    // Build the repalcement string and add it to the replacement dictionary
                    var replacementKey = buildReplacementClass(charArray, atChar);
                    try
                    {
                        replacementStrings.Add(replacementKey.key, replacementKey.replacementString);
                    }
                    catch (Exception)
                    {
                        // TODO: Handle error where multiple replacement strings are defined for the same key
                    }
                    continue;
                }
                newArticle.Type = workingString.Trim();

                // Build the Name/Title
                workingString = string.Empty;
                while (atChar < arrayLength)
                {
                    if (charArray[atChar].Equals(','))
                    {
                        atChar++;
                        break;
                    }
                    workingString += charArray[atChar];
                    atChar++;
                }

                newArticle.BibtexTitle = workingString;
                // We are now past the string "@<articleType>{<ArticleName>,"
                // Find each of the fields we need a string <FieldName> { <fieldtext>}. A comma "," means we have another field coming, else we should have a } to end the current article
                // The field ends at the first unopened "}" If we see a "{" we need to see another closure to ensure compliance.
                fields = getFields(fields, charArray, atChar, arrayLength);

                // We have now got all fields. Add to model
                foreach (var field in fields)
                {
                    newArticle.Fields.Add(new ArticleField { ID = Guid.NewGuid(), Type = field.Key, Value = field.Value });
                }

                //Insert into collection of all articles
                allArticles.Add(newArticle);
            }

            var legalArticles = new List<Article>();
            // Replace replacementkeys with the appropriate replacement strings
            foreach (var article in allArticles)
            {
                foreach (var field in article.Fields)
                {
                    foreach (var replacementpair in replacementStrings)
                    {
                        field.Value = field.Value.Replace(replacementpair.Key, replacementpair.Value);
                    }
                }

                // Build the Article
                BuildArticleFromFields(article);
                if (!string.IsNullOrEmpty(article.Author) && !string.IsNullOrEmpty(article.Title))
                {
                    legalArticles.Add(article);
                }
            }

            // Return the list of articles
            return legalArticles;

        }

        /// <summary>
        /// Builds an article from its fields
        /// </summary>
        /// <param name="article"></param>
        private void BuildArticleFromFields(Article article)
        {
            foreach (var field in article.Fields)
            {
                // When using quotation amrks as delimiters, commas get into the field description as leading for some reason. Nuke them.
                field.Value.TrimStart(',');
                field.Value.Trim();
                // Remove the = sign
                field.Type = field.Type.Replace("=", "").Trim();
                switch (field.Type.ToLower())
                {
                    case "author":
                        article.Author = field.Value;
                        break;
                    case "year":
                        article.PublicationDate = field.Value;
                        break; ;
                    case "abstract":
                        article.Abstract = field.Value;
                        break;
                    case "keywords":
                        article.KeyWords = article.KeyWords + field.Value;
                        break;
                    case "author_keywords":
                        article.KeyWords = article.KeyWords + field.Value;
                        break;
                    case "source":
                        article.Database = field.Value;
                        break;
                    case "title":
                        article.Title = field.Value;
                        break;
                    case "booktitle":
                        article.Title = field.Value;
                        break;
                }
            }
        }

        /// <summary>
        /// Recursive method to find all fields
        /// </summary>
        /// <param name="fieldsSoFar">Fields found so far</param>
        /// <param name="charArray">The char[] containing the bibtex entry</param>
        /// <param name="atChar">Current position in the char[]</param>
        /// <param name="arrayLength">Length of the char[]. No need to recompute.</param>
        /// <returns>A set of fields for the remaining char[]</returns>
        private Dictionary<string, string> getFields(
            Dictionary<string, string> fieldsSoFar,
            char[] charArray,
            int atChar,
            int arrayLength)
        {
            // Build the Fieldname (key)
            var fieldName = string.Empty;
            var extraClosesNeeded = 0;
            var quotationDelimiter = false;
            while (atChar < arrayLength)
            {
                // Handle EndOfFile preceded by a comma
                if (charArray[atChar].Equals('}'))
                {
                    return fieldsSoFar;
                }
                if (charArray[atChar].Equals('{'))
                {
                    atChar++;
                    break;
                }

                if (charArray[atChar].Equals('"'))
                {
                    atChar++;
                    quotationDelimiter = true;
                    break;
                }
                fieldName += charArray[atChar];
                atChar++;
            }

            // Lowercase for consistency
            fieldName = fieldName.ToLower();

            // Build the fieldtext
            var fieldText = string.Empty;
            while (atChar < arrayLength)
            {
                // Handle strings where quotations are used as delimiters. We close now!
                if(charArray[atChar].Equals('"') && quotationDelimiter == true)
                {
                    // We're done with this field description. Add it to the list. Because some sites like Science Direct are morons who include multiple
                    // tuples with the same key, we need to handle this.
                    if (fieldsSoFar.ContainsKey(fieldName))
                    {
                        fieldsSoFar[fieldName] = fieldsSoFar[fieldName] + ", " + fieldText;
                    }
                    else
                    {
                        fieldsSoFar.Add(fieldName, fieldText);
                    }
                    while (atChar < arrayLength)
                    {
                        // See if there are more fields
                        if (charArray[atChar].Equals(','))
                        {
                            atChar++;
                            return getFields(fieldsSoFar, charArray, atChar, arrayLength);
                        }
                        atChar++;
                    }

                    // Will be hit if there are no more fields. Allows for missing closing bracket or partially malformed BibTex. Return what we have for robustness.
                    return fieldsSoFar;
                }
                // Handle strings with brackets elegantly
                if (charArray[atChar].Equals('{'))
                {
                    extraClosesNeeded++;
                }
                if (charArray[atChar].Equals('}'))
                {
                    // More elegant handling
                    if (extraClosesNeeded > 0)
                    {
                        extraClosesNeeded--;
                    }
                    else if (quotationDelimiter == false)// Time to close this puppy. Iterate through to next Field or full closure
                    {
                        // We're done with this field description. Add it to the list. Because some sites like Science Direct are morons who include multiple
                        // tuples with the same key, we need to handle this.
                        if (fieldsSoFar.ContainsKey(fieldName))
                        {
                            fieldsSoFar[fieldName] = fieldsSoFar[fieldName] + ", " + fieldText;
                        }
                        else
                        {
                            fieldsSoFar.Add(fieldName, fieldText);
                        }
                        while (atChar < arrayLength)
                        {
                            // See if there are more fields
                            if (charArray[atChar].Equals(','))
                            {
                                atChar++;
                                return getFields(fieldsSoFar, charArray, atChar, arrayLength);
                            }
                            atChar++;
                        }

                        // Will be hit if there are no more fields. Allows for missing closing bracket or partially malformed BibTex. Return what we have for robustness.
                        return fieldsSoFar;
                    }
                }
                fieldText += charArray[atChar];
                atChar++;
            }

            // We should never get here.
            return fieldsSoFar;
        }


        /// <summary>
        /// Builds a string from the chararray
        /// </summary>
        /// <param name="charArray">Charray to build a string from</param>
        /// <param name="atChar">The point to start building from</param>
        /// <returns></returns>
        private ReplacementPair buildReplacementClass(
            char[] charArray,
            int atChar)
        {
            // Build the fieldtext
            var arrayLength = charArray.Length;
            var rPair = new ReplacementPair();

            // build key
            while (atChar < arrayLength)
            {
                if (charArray[atChar].Equals('='))
                {
                    //We're done with the key
                    atChar++;
                    break;
                }
                rPair.key += charArray[atChar];
                atChar++;
            }

            // Get to replacement string
            while (atChar < arrayLength)
            {
                if (charArray[atChar].Equals('"'))
                {
                    //We're now entering the string
                    atChar++;
                    break;
                }
                atChar++;
            }

            // build replacement string
            while (atChar < arrayLength)
            {
                if (charArray[atChar].Equals('"'))
                {
                    //We're done with this field description. Add it to the list.
                    return rPair;
                }
                rPair.replacementString += charArray[atChar];
                atChar++;
            }
            // We should never get here
            return rPair;
        }

        private class ReplacementPair
        {
            public string key { get; set; }
            public string replacementString { get; set; }
        }
    }
}